import logging
import string

import util.json as json

import guid
import version
import sync
import api

log = logging.getLogger('atom')

ATOM_METADATA_TABLE = 'neon_atom_meta'
ATOM_TYPE_TABLE = 'neon_atom_types'

# ----------------------------------------------------------------------------
class AtomType:
    """Metaclass which represents an AtomType in the system."""

    class Intersect:
        def __init__(self, table, query, fields):
            self.table_  = table
            self.query_  = query
            self.fields_ = fields

        def intersect_query(self, a):
            if len(a.fields[self.table_]) == 0:
                return None
            data = a.fields[self.table_][0]
            return (self.query_, tuple([data[i] for i in self.fields_]))

    def __init__(self, name):        
        self.name_      = name
        self.tables_    = []
        self.intersect_ = {}

    def __repr__(self):
        return '<AtomType %s>' % self.name_

    def name(self):
        return self.name_

    def tables(self):
        return self.tables_

    def add_table(self, tablename, intersect = None):
	"""intersect is a pair of SQL stmt, list of fields"""
        if tablename in self.tables_:
            log.warn('table %s already covered by type %s' % (table, self))
        self.tables_.append(tablename)
        if intersect != None:
            isect = AtomType.Intersect(tablename, intersect[0], intersect[1])
            self.intersect_[tablename] = isect
        else:
            self.intersect_[tablename] = None

    def intersect_query(self, a):            
        return [self.intersect_[t].intersect_query(a)
                for t in self.tables_
                if self.intersect_[t] != None]

    def validate(self, db):
        """Validate if the DB backing this type has the right tables"""
        assert db.table_exists(ATOM_METADATA_TABLE)
        for t in self.tables_:
            if not db.table_exists(t):
                log.warn("Type %s references table %s which can't be found" %
                          (self, t))
                return False
        return True
        
    def to_raw(self):
        return {'name'             : self.name_,
                'tables'           : self.tables_,
                'intersect'        : self.intersect_,
                'intersect_fields' : self.intersect_fields_}

# ----------------------------------------------------------------------------
class Atom:
    """A handle to an atom in the system."""

    def __init__(self, type):
        assert isinstance(type, AtomType)
        self.type = type
        self.guid = guid.UNSET
        self.vv = version.Vector()
        self.fields = {}
        
    def __repr__(self):
        return "<Atom %s %s, <%s> >" % \
               (self.type.name(), self.get_guid(), self.get_version())

    def __eq__(self, other):
        if type(self) != type(other):
            return False
        return ((self.type == other.type) and
                (self.guid == other.guid) and
                (self.vv == other.vv) and
                (self.fields == other.fields))

    def get_guid(self):
        return self.guid

    def get_version(self):
        return self.vv

    def set_guid(self, g):
        assert isinstance(g, guid.Guid)
        self.guid = g
        # XXX/bowei probably better way to do this
        for t in self.type.tables():
            if t in self.fields:
                for i in self.fields[t]:
                    i['guid'] = str(g)
        
    def set_version(self, ver):
        assert isinstance(ver, version.Vector)
        self.vv = ver

    def to_raw(self):
        # XXX/bowei -- todo
        ret = {'type'   : self.type.name(),
               'guid'   : str(self.guid),
               'vv'     : str(self.vv),
               'fields' : self.fields}
        return ret
    
    def from_raw(self, raw):
        assert self.type.name() == raw['type']
        self.guid = guid.from_raw(raw['guid'])
        self.vv   = version.from_raw(raw['vv'])
        self.fields = raw['fields']
        # XXX/bowei ugh
        self.set_guid(self.guid)
                
    def is_valid(self):
        # XXX/bowei -- todo
        return True

# ----------------------------------------------------------------------------
class AtomStore:
    """Stores mappings between guid (id, ver) -> atom snapshots and the
    Atom object types."""

    def __init__(self, sync_inst, init_meta, type_file):
        self.types_ = {}
        self.table_2_types_ = {}
        self.sync_ = sync_inst
        self.db_ = sync_inst.db_

        # XXX/bowei -- test hook
        # XXX/bowei -- can this get out of sync with the new atoms in
        # the neon_atom_meta table??
        # put the max current local ver in share DB
        next_ver = max(1, self.sync_.get_own_vs(). get(self.sync_.own_id()).max())
        self.db_.exec_sql("INSERT INTO neon_metadata VALUES ('next_ver', '%d');" %
                          next_ver)
        log.info('next_ver = %d' % next_ver)
        
        if init_meta:	
            self.init_guid_type_table()
        self.create_clean_type_table()
        self.load_types(type_file)

        log.debug('----- Registered Types -----')
        for t in self.types_.keys():
            log.debug('TYPE=%s' % t)
            log.debug('%s, %s' % (self.types_[t], self.types_[t].intersect_))

    def db(self):
        return self.db_

    def get_type(self, typename):
        return self.types_[typename]

    def get_typenames(self):
        return self.types_.keys()

    def get_types_from_table(self, table):
        if table in self.table_2_types_:
            return self.table_2_types_[table]
        return None

    def get_new_guid(self):
        own_id = 'testing-id'
	if self.sync_:
            own_id = self.sync_.own_id()
        return guid.Guid(own_id, self.get_new_guid_ver_())
         
    def get_new_guid_ver_(self):
        r = self.db_.exec_sql("SELECT value FROM neon_metadata WHERE name = 'next_ver'")
        next_ver = int(r[0][0])
        self.db_.exec_sql("UPDATE neon_metadata SET value = '%d' "
                          "WHERE name = 'next_ver'" % next_ver + 1)
        return next_ver
    
    def reg_type(self, t):
        if t.name() in self.types_:
            log.warn('Type %s is already registered, ignoring' % t.name())
            return  # XXX/bowei -- error code here
        self.types_[t.name()] = t
        # XXX/bowei -- WTF happened to the validate??? it doesn't work
        # anymore
        # t.validate(self.db_)

        # Put the table -> type mappings in place
        for tab in t.tables():
            if tab in self.table_2_types_:
                self.table_2_types_[tab].append(t)
            else:
                self.table_2_types_[tab] = [t]
        log.info('Registered type "%s"' % t.name())

    def init_guid_type_table(self):
        self.db_.exec_sql('DROP TABLE IF EXISTS %s' % ATOM_METADATA_TABLE)
        self.db_.commit()
        # XXX/bowei -- create index on guid
        self.db_.exec_sql('''
            CREATE TABLE %s (
                guid    VARCHAR(50),
                vv      VARCHAR(255),
                type    VARCHAR(50),
                deleted BOOLEAN,
		local   BOOLEAN
            );''' % ATOM_METADATA_TABLE)
        self.db_.commit()

    def create_clean_type_table(self):
        self.db_.exec_sql('DROP TABLE IF EXISTS %s' % ATOM_TYPE_TABLE)
        self.db_.exec_sql('''
            CREATE TABLE %s (
                type             VARCHAR(50),
                tablename        VARCHAR(50),
                guid_field       VARCHAR(50),
                intersect_query  VARCHAR(300),
                intersect_fields VARCHAR(300)
            );
        ''' % ATOM_TYPE_TABLE)
        self.db_.commit()

    def put_types_into_db(self):        
        for typename in self.types_:
            t = self.types_[typename]
            for tablename in t.tables_:
                isect = t.intersect_[tablename]
                self.db_.exec_sql("INSERT INTO %s VALUES ('%s', '%s', '%s', '%s', '%s');" %
                                  (ATOM_TYPE_TABLE,
                                   typename, tablename, '',
                                   self.db_.escape_str(isect.query_),
                                   string.join(isect.fields_, ' ')))
                self.db_.commit()
                
    def load_types(self, types_module):
        log.info('Loading types from %s' % types_module)

        if types_module != None:
            modname = types_module.split('.')[-1]
            types_module = __import__(types_module, globals(), locals(), [modname], 0)
            t_cfg = types_module.TypesConfig(self)
            # XXX/bowei -- finish this import code
            self.put_types_into_db()
        else:
            log.info('No type file specified, starting with no types')

    def get_new_local_atoms(self):
	"""Scan for new local to signal to the iterators."""
	# Invariant we rely on is that the verset for the local atoms
	# never has any gaps in it, so we just need to look at the max
	rows = self.db_.query('SELECT guid FROM %s WHERE local = 1' %
                              ATOM_METADATA_TABLE)
	return [guid.from_raw(r[0]) for r in rows]

    def set_local_seen(self, g):
	"""Set the guid table state that we have processed the local version"""
	self.db_.exec_sql("UPDATE %s SET local = 0 WHERE guid = '%s'" %
                          (ATOM_METADATA_TABLE, g.to_raw()))
        
    def get_atom(self, g):
        """@return Handle to atom. None if Atom does not exist."""
        log.debug('get_atom(%s)' % g)
        rows = self.db_.query("SELECT * FROM %s WHERE guid = '%s'" %
                              (ATOM_METADATA_TABLE, str(g)))
        if len(rows) == 0:
            log.debug('get_atom(%s) - None' % g)
            return None
        assert len(rows) == 1
        guid_str, vv_str, typename, deleted, _ = rows[0]
        if deleted:
            return None
        ty = self.types_[typename]
        a = Atom(ty)
        a.set_guid(guid.from_raw(guid_str))
        a.set_version(version.from_raw(vv_str))
        
        for t in ty.tables():
            rows = self.db_.query("SELECT * FROM %s WHERE guid = '%s'" % (t, g))
            desc = self.db_.table_schema(t)
            a.fields[t] = []
            for r in rows:
                v = {}
                for i in xrange(len(desc)):
                    v[desc[i][0]] = r[i]
                a.fields[t].append(v)
        log.debug('get_atom(%s) - %s' % (g, a))
        return a

    def del_atom(self, a, tombstone = True):
        log.debug('del_atom(%s)' % a.guid)
        for t in a.type.tables():
            self.db_.exec_sql("DELETE FROM %s WHERE guid = '%s'" % (t, a.guid))
        if tombstone:
            self.db_.exec_sql("UPDATE %s SET deleted = 1 WHERE guid = '%s'" %
                              (ATOM_METADATA_TABLE, a.guid))
        else:
            self.db_.exec_sql("DELETE FROM %s WHERE guid = '%s'" %
                              (ATOM_METADATA_TABLE, a.guid))
            
    def insert_fields_sql_(self, tablename, len):
        sql = 'INSERT INTO %s VALUES (' % tablename
        first = True
        for i in xrange(len):
            if not first:
                sql = sql + ','
            sql = sql + self.db_.placeholder_str()
            first = False
        sql = sql + ')'
        return sql

    def put_atom(self, a):
        """Put the atom into the database. This will first integrate
        the atom into the DB.
        
        @return api.OK, api.OBSOLETE, api.CONFLICT"""
        log.debug('put_atom(%s)' % a)

        ret = self.integrate(a)
        self.db_.exec_sql("INSERT INTO %s"
                          " VALUES ('%s', '%s', '%s', %d, %d)" %
                          (ATOM_METADATA_TABLE,
                           str(a.get_guid()), str(a.get_version()), a.type.name(), 
                           0, a.get_guid().id_ == self.sync_.own_id()))
        for t in a.fields.keys():
            s = self.db_.table_schema(t)
            table = a.fields[t]
            for r in table:
                query_values = [r[e] for e in s]
                self.db_.exec_sql(self.insert_fields_sql_(t, len(query_values)),
                                 query_values)
        return ret

    def integrate(self, a):
        log.debug('integrate(%s)' % a)

        # get all atoms that are intersecting
        i_queries = a.type.intersect_query(a)

        guids = []
        for q in i_queries:
            rows = self.db_.query(*q)
            guids = guids + [r[0] for r in rows]

        atoms = []
        for g in set(guids):
            # XXX/bowei -- make this more efficient making atom
            # attributes not load until needed
            tmp_a = self.get_atom(g)
            if tmp_a is None:
                log.warn("guid %s doesn't exist" % g)
            else:
                atoms.append(tmp_a)
        log.debug('intersection %s' % atoms)
        
        ret = 0
        for other_a in atoms:
            if a.vv <= other_a.vv:
                log.warn('atom %s geq, %s <= %s' %
                         (other_a.guid, a.vv, other_a.vv))
                ret = ret | api.OBSOLETE
            elif other_a.vv < a.vv:
                self.del_atom(other_a, False)
                log.debug('%s was obsolete, and deleted' % other_a.guid)
            elif other_a.vv != a.vv:
                log.debug('%s is in conflict, %s <> %s' %
                          (other_a.guid, other_a.vv, a.vv))
                ret = ret | api.CONFLICT
        return ret
